Розкрийте можливості допоміжних функцій асинхронних генераторів JavaScript для ефективного створення, трансформації та керування потоками. Розгляньте практичні приклади та реальні сценарії для створення надійних асинхронних додатків.
Допоміжні функції асинхронних генераторів JavaScript: Опанування створення та керування потоками
Асинхронне програмування в JavaScript значно еволюціонувало за останні роки. З появою асинхронних генераторів та асинхронних ітераторів розробники отримали потужні інструменти для роботи з потоками асинхронних даних. Тепер допоміжні функції асинхронних генераторів JavaScript ще більше розширюють ці можливості, надаючи більш простий та виразний спосіб створювати, трансформувати та керувати асинхронними потоками даних. Цей посібник розглядає основи допоміжних функцій асинхронних генераторів, заглиблюється в їхню функціональність та демонструє їх практичне застосування на зрозумілих прикладах.
Розуміння асинхронних генераторів та ітераторів
Перш ніж заглиблюватися в допоміжні функції асинхронних генераторів, важливо зрозуміти основні концепції асинхронних генераторів та асинхронних ітераторів.
Асинхронні генератори
Асинхронний генератор — це функція, яку можна призупинити та відновити, повертаючи значення асинхронно. Вона дозволяє генерувати послідовність значень з часом, не блокуючи основний потік. Асинхронні генератори визначаються за допомогою синтаксису async function*.
Приклад:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 500)); // Симуляція асинхронної операції
yield i;
}
}
// Використання
const sequence = generateSequence(1, 5);
Асинхронні ітератори
Асинхронний ітератор — це об'єкт, який надає метод next(), що повертає проміс, який вирішується в об'єкт, що містить наступне значення в послідовності та властивість done, яка вказує, чи вичерпана послідовність. Асинхронні ітератори обробляються за допомогою циклів for await...of.
Приклад:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 500));
yield i;
}
}
async function consumeSequence() {
const sequence = generateSequence(1, 5);
for await (const value of sequence) {
console.log(value);
}
}
consumeSequence();
Знайомство з допоміжними функціями асинхронних генераторів
Допоміжні функції асинхронних генераторів — це набір методів, які розширюють функціональність прототипів асинхронних генераторів. Вони надають зручні способи маніпулювання асинхронними потоками даних, роблячи код більш читабельним та легким для підтримки. Ці допоміжні функції працюють ліниво, тобто вони обробляють дані лише тоді, коли це необхідно, що може покращити продуктивність.
Наступні допоміжні функції асинхронних генераторів зазвичай доступні (залежно від середовища JavaScript та поліфілів):
mapfiltertakedropflatMapreducetoArrayforEach
Детальний огляд допоміжних функцій асинхронних генераторів
1. `map()`
Допоміжна функція map() трансформує кожне значення в асинхронній послідовності, застосовуючи надану функцію. Вона повертає новий асинхронний генератор, який видає трансформовані значення.
Синтаксис:
asyncGenerator.map(callback)
Приклад: Перетворення потоку чисел на їхні квадрати.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const squares = numbers.map(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100)); // Симуляція асинхронної операції
return num * num;
});
for await (const square of squares) {
console.log(square);
}
}
processNumbers();
Приклад з реального життя: Уявіть, що ви отримуєте дані користувачів з кількох API і вам потрібно перетворити ці дані в єдиний формат. map() можна використовувати для асинхронного застосування функції трансформації до кожного об'єкта користувача.
async function* fetchUsersFromMultipleAPIs(apiEndpoints) {
for (const endpoint of apiEndpoints) {
const response = await fetch(endpoint);
const data = await response.json();
for (const user of data) {
yield user;
}
}
}
async function processUsers() {
const apiEndpoints = [
'https://api.example.com/users1',
'https://api.example.com/users2'
];
const users = fetchUsersFromMultipleAPIs(apiEndpoints);
const normalizedUsers = users.map(async (user) => {
// Нормалізація формату даних користувача
return {
id: user.userId || user.id,
name: user.fullName || user.name,
email: user.emailAddress || user.email
};
});
for await (const normalizedUser of normalizedUsers) {
console.log(normalizedUser);
}
}
2. `filter()`
Допоміжна функція filter() створює новий асинхронний генератор, який видає лише ті значення з вихідної послідовності, які задовольняють надану умову. Це дозволяє вибірково включати значення до результуючого потоку.
Синтаксис:
asyncGenerator.filter(callback)
Приклад: Фільтрація потоку чисел для включення лише парних чисел.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 10);
const evenNumbers = numbers.filter(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100));
return num % 2 === 0;
});
for await (const evenNumber of evenNumbers) {
console.log(evenNumber);
}
}
processNumbers();
Приклад з реального життя: Обробка потоку записів журналу та фільтрація записів за рівнем їх важливості. Наприклад, обробка лише помилок та попереджень.
async function* readLogFile(filePath) {
// Симуляція асинхронного читання файлу журналу рядок за рядком
const logEntries = [
{ timestamp: '...', level: 'INFO', message: '...' },
{ timestamp: '...', level: 'ERROR', message: '...' },
{ timestamp: '...', level: 'WARNING', message: '...' },
{ timestamp: '...', level: 'INFO', message: '...' },
{ timestamp: '...', level: 'ERROR', message: '...' }
];
for (const entry of logEntries) {
await new Promise(resolve => setTimeout(resolve, 50));
yield entry;
}
}
async function processLogs() {
const logEntries = readLogFile('path/to/log/file.log');
const errorAndWarningLogs = logEntries.filter(async (entry) => {
return entry.level === 'ERROR' || entry.level === 'WARNING';
});
for await (const log of errorAndWarningLogs) {
console.log(log);
}
}
3. `take()`
Допоміжна функція take() створює новий асинхронний генератор, який видає лише перші n значень з вихідної послідовності. Це корисно для обмеження кількості оброблюваних елементів з потенційно нескінченного або дуже великого потоку.
Синтаксис:
asyncGenerator.take(n)
Приклад: Отримання перших 3 чисел з потоку чисел.
async function* generateNumbers(start) {
let i = start;
while (true) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i++;
}
}
async function processNumbers() {
const numbers = generateNumbers(1);
const firstThree = numbers.take(3);
for await (const num of firstThree) {
console.log(num);
}
}
processNumbers();
Приклад з реального життя: Відображення топ-5 результатів пошуку з асинхронного API пошуку.
async function* search(query) {
// Симуляція отримання результатів пошуку з API
const results = [
{ title: 'Result 1', url: '...' },
{ title: 'Result 2', url: '...' },
{ title: 'Result 3', url: '...' },
{ title: 'Result 4', url: '...' },
{ title: 'Result 5', url: '...' },
{ title: 'Result 6', url: '...' }
];
for (const result of results) {
await new Promise(resolve => setTimeout(resolve, 100));
yield result;
}
}
async function displayTopSearchResults(query) {
const searchResults = search(query);
const top5Results = searchResults.take(5);
for await (const result of top5Results) {
console.log(result);
}
}
4. `drop()`
Допоміжна функція drop() створює новий асинхронний генератор, який пропускає перші n значень з вихідної послідовності та видає решту значень. Це протилежність take() і корисно для ігнорування початкових частин потоку.
Синтаксис:
asyncGenerator.drop(n)
Приклад: Пропуск перших 2 чисел з потоку чисел.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const remainingNumbers = numbers.drop(2);
for await (const num of remainingNumbers) {
console.log(num);
}
}
processNumbers();
Приклад з реального життя: Пагінація великого набору даних, отриманого з API, з пропуском вже відображених результатів.
async function* fetchData(url, pageSize, pageNumber) {
const offset = (pageNumber - 1) * pageSize;
// Симуляція отримання даних зі зміщенням
const data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' },
{ id: 7, name: 'Item 7' },
{ id: 8, name: 'Item 8' }
];
const pageData = data.slice(offset, offset + pageSize);
for (const item of pageData) {
await new Promise(resolve => setTimeout(resolve, 100));
yield item;
}
}
async function displayPage(pageNumber) {
const pageSize = 3;
const allData = fetchData('api/data', pageSize, pageNumber);
const page = allData.drop((pageNumber - 1) * pageSize); // пропустити елементи з попередніх сторінок
const results = page.take(pageSize);
for await (const item of results) {
console.log(item);
}
}
// Приклад використання
displayPage(2);
5. `flatMap()`
Допоміжна функція flatMap() трансформує кожне значення в асинхронній послідовності, застосовуючи функцію, яка повертає асинхронну ітеровану послідовність (Async Iterable). Потім вона "розгладжує" результуючу асинхронну ітеровану послідовність в єдиний асинхронний генератор. Це корисно для перетворення кожного значення в потік значень, а потім об'єднання цих потоків.
Синтаксис:
asyncGenerator.flatMap(callback)
Приклад: Перетворення потоку речень на потік слів.
async function* generateSentences() {
const sentences = [
'This is the first sentence.',
'This is the second sentence.',
'This is the third sentence.'
];
for (const sentence of sentences) {
await new Promise(resolve => setTimeout(resolve, 200));
yield sentence;
}
}
async function* stringToWords(sentence) {
const words = sentence.split(' ');
for (const word of words) {
await new Promise(resolve => setTimeout(resolve, 50));
yield word;
}
}
async function processSentences() {
const sentences = generateSentences();
const words = sentences.flatMap(async (sentence) => {
return stringToWords(sentence);
});
for await (const word of words) {
console.log(word);
}
}
processSentences();
Приклад з реального життя: Отримання коментарів до кількох дописів у блозі та об'єднання їх в єдиний потік для обробки.
async function* fetchBlogPostIds() {
const blogPostIds = [1, 2, 3]; // Симуляція отримання ID дописів блогу з API
for (const id of blogPostIds) {
await new Promise(resolve => setTimeout(resolve, 100));
yield id;
}
}
async function* fetchCommentsForPost(postId) {
// Симуляція отримання коментарів до допису блогу з API
const comments = [
{ postId: postId, text: `Comment 1 for post ${postId}` },
{ postId: postId, text: `Comment 2 for post ${postId}` }
];
for (const comment of comments) {
await new Promise(resolve => setTimeout(resolve, 50));
yield comment;
}
}
async function processComments() {
const postIds = fetchBlogPostIds();
const allComments = postIds.flatMap(async (postId) => {
return fetchCommentsForPost(postId);
});
for await (const comment of allComments) {
console.log(comment);
}
}
6. `reduce()`
Допоміжна функція reduce() застосовує функцію до акумулятора та кожного значення асинхронного генератора (зліва направо), щоб звести його до одного значення. Це корисно для агрегації даних з асинхронного потоку.
Синтаксис:
asyncGenerator.reduce(callback, initialValue)
Приклад: Обчислення суми чисел у потоці.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const sum = await numbers.reduce(async (accumulator, num) => {
await new Promise(resolve => setTimeout(resolve, 100));
return accumulator + num;
}, 0);
console.log('Sum:', sum);
}
processNumbers();
Приклад з реального життя: Обчислення середнього часу відповіді для серії викликів API.
async function* fetchResponseTimes(apiEndpoints) {
for (const endpoint of apiEndpoints) {
const startTime = Date.now();
try {
await fetch(endpoint);
const endTime = Date.now();
const responseTime = endTime - startTime;
await new Promise(resolve => setTimeout(resolve, 50));
yield responseTime;
} catch (error) {
console.error(`Error fetching ${endpoint}: ${error}`);
yield 0; // Або обробіть помилку належним чином
}
}
}
async function calculateAverageResponseTime() {
const apiEndpoints = [
'https://api.example.com/endpoint1',
'https://api.example.com/endpoint2',
'https://api.example.com/endpoint3'
];
const responseTimes = fetchResponseTimes(apiEndpoints);
let count = 0;
const sum = await responseTimes.reduce(async (accumulator, time) => {
count++;
return accumulator + time;
}, 0);
const average = count > 0 ? sum / count : 0;
console.log(`Average response time: ${average} ms`);
}
7. `toArray()`
Допоміжна функція toArray() споживає асинхронний генератор і повертає проміс, який вирішується в масив, що містить усі значення, видані генератором. Це корисно, коли вам потрібно зібрати всі значення з потоку в один масив для подальшої обробки.
Синтаксис:
asyncGenerator.toArray()
Приклад: Збір чисел з потоку в масив.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
const numberArray = await numbers.toArray();
console.log('Number Array:', numberArray);
}
processNumbers();
Приклад з реального життя: Збір усіх елементів з пагінованого API в єдиний масив для фільтрації або сортування на стороні клієнта.
async function* fetchAllItems(apiEndpoint) {
let pageNumber = 1;
const pageSize = 100; // Налаштуйте відповідно до лімітів пагінації API
while (true) {
const url = `${apiEndpoint}?page=${pageNumber}&pageSize=${pageSize}`;
const response = await fetch(url);
const data = await response.json();
if (!data || data.length === 0) {
break; // Більше немає даних
}
for (const item of data) {
await new Promise(resolve => setTimeout(resolve, 50));
yield item;
}
pageNumber++;
}
}
async function processAllItems() {
const apiEndpoint = 'https://api.example.com/items';
const allItems = fetchAllItems(apiEndpoint);
const itemsArray = await allItems.toArray();
console.log(`Fetched ${itemsArray.length} items.`);
// Подальшу обробку можна виконувати над `itemsArray`
}
8. `forEach()`
Допоміжна функція forEach() виконує надану функцію один раз для кожного значення в асинхронному генераторі. На відміну від інших допоміжних функцій, forEach() не повертає новий асинхронний генератор; вона використовується для виконання побічних ефектів для кожного значення.
Синтаксис:
asyncGenerator.forEach(callback)
Приклад: Виведення кожного числа з потоку в консоль.
async function* generateNumbers(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 200));
yield i;
}
}
async function processNumbers() {
const numbers = generateNumbers(1, 5);
await numbers.forEach(async (num) => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Number:', num);
});
}
processNumbers();
Приклад з реального життя: Надсилання оновлень у реальному часі до користувацького інтерфейсу під час обробки даних з потоку.
async function* fetchRealTimeData(dataSource) {
//Симуляція отримання даних у реальному часі (напр., ціни на акції).
const dataStream = [
{ timestamp: new Date(), price: 100 },
{ timestamp: new Date(), price: 101 },
{ timestamp: new Date(), price: 102 }
];
for (const dataPoint of dataStream) {
await new Promise(resolve => setTimeout(resolve, 500));
yield dataPoint;
}
}
async function updateUI() {
const realTimeData = fetchRealTimeData('stock-api');
await realTimeData.forEach(async (data) => {
//Симуляція оновлення UI
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Updating UI with data: ${JSON.stringify(data)}`);
// Код для фактичного оновлення UI буде тут.
});
}
Комбінування допоміжних функцій асинхронних генераторів для складних конвеєрів даних
Справжня сила допоміжних функцій асинхронних генераторів полягає в їхній здатності об'єднуватися в ланцюжки для створення складних конвеєрів даних. Це дозволяє виконувати кілька перетворень та операцій над асинхронним потоком у стислий та читабельний спосіб.
Приклад: Фільтрація потоку чисел для включення лише парних чисел, потім піднесення їх до квадрату, і, нарешті, отримання перших 3 результатів.
async function* generateNumbers(start) {
let i = start;
while (true) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i++;
}
}
async function processNumbers() {
const numbers = generateNumbers(1);
const processedNumbers = numbers
.filter(async (num) => num % 2 === 0)
.map(async (num) => num * num)
.take(3);
for await (const num of processedNumbers) {
console.log(num);
}
}
processNumbers();
Приклад з реального життя: Отримання даних користувачів, фільтрація користувачів за їхнім місцезнаходженням, перетворення їхніх даних, щоб включити лише релевантні поля, а потім відображення перших 10 користувачів на карті.
async function* fetchUsers() {
// Симуляція отримання користувачів з бази даних або API
const users = [
{ id: 1, name: 'John Doe', location: 'New York', email: 'john.doe@example.com' },
{ id: 2, name: 'Jane Smith', location: 'London', email: 'jane.smith@example.com' },
{ id: 3, name: 'Ken Tan', location: 'Singapore', email: 'ken.tan@example.com' },
{ id: 4, name: 'Alice Jones', location: 'New York', email: 'alice.jones@example.com' },
{ id: 5, name: 'Bob Williams', location: 'London', email: 'bob.williams@example.com' },
{ id: 6, name: 'Siti Rahman', location: 'Singapore', email: 'siti.rahman@example.com' },
{ id: 7, name: 'Ahmed Khan', location: 'Dubai', email: 'ahmed.khan@example.com' },
{ id: 8, name: 'Maria Garcia', location: 'Madrid', email: 'maria.garcia@example.com' },
{ id: 9, name: 'Li Wei', location: 'Shanghai', email: 'li.wei@example.com' },
{ id: 10, name: 'Hans Müller', location: 'Berlin', email: 'hans.muller@example.com' },
{ id: 11, name: 'Emily Chen', location: 'Sydney', email: 'emily.chen@example.com' }
];
for (const user of users) {
await new Promise(resolve => setTimeout(resolve, 50));
yield user;
}
}
async function displayUsersOnMap(location, maxUsers) {
const users = fetchUsers();
const usersForMap = users
.filter(async (user) => user.location === location)
.map(async (user) => ({
id: user.id,
name: user.name,
location: user.location
}))
.take(maxUsers);
console.log(`Displaying up to ${maxUsers} users from ${location} on the map:`);
for await (const user of usersForMap) {
console.log(user);
}
}
// Приклади використання:
displayUsersOnMap('New York', 2);
displayUsersOnMap('London', 5);
Поліфіли та підтримка браузерами
Підтримка допоміжних функцій асинхронних генераторів може відрізнятися залежно від середовища JavaScript. Якщо вам потрібно підтримувати старіші браузери або середовища, можливо, доведеться використовувати поліфіли. Поліфіл надає відсутню функціональність, реалізуючи її на JavaScript. Існує кілька бібліотек поліфілів для допоміжних функцій асинхронних генераторів, наприклад, core-js.
Приклад використання core-js:
// Імпортуйте необхідні поліфіли
require('core-js/features/async-iterator/map');
require('core-js/features/async-iterator/filter');
// ... імпортуйте інші необхідні допоміжні функції
Обробка помилок
При роботі з асинхронними операціями вкрай важливо правильно обробляти помилки. З допоміжними функціями асинхронних генераторів обробку помилок можна виконати за допомогою блоків try...catch всередині асинхронних функцій, що використовуються в допоміжних функціях.
Приклад: Обробка помилок при отриманні даних в операції map().
async function* fetchData(urls) {
for (const url of urls) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
yield data;
} catch (error) {
console.error(`Error fetching data from ${url}: ${error}`);
yield null; // Або обробіть помилку належним чином, напр., повертаючи об'єкт помилки
}
}
}
async function processData() {
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
];
const dataStream = fetchData(urls);
const processedData = dataStream.map(async (data) => {
if (data === null) {
return null; // Прокинути помилку далі
}
// Обробити дані
return data;
});
for await (const item of processedData) {
if (item === null) {
console.log('Skipping item due to error');
continue;
}
console.log('Processed Item:', item);
}
}
processData();
Найкращі практики та рекомендації
- Ліниві обчислення: Допоміжні функції асинхронних генераторів обчислюються ліниво, тобто вони обробляють дані лише тоді, коли їх запитують. Це може покращити продуктивність, особливо при роботі з великими наборами даних.
- Обробка помилок: Завжди правильно обробляйте помилки всередині асинхронних функцій, що використовуються в допоміжних функціях.
- Поліфіли: Використовуйте поліфіли, коли це необхідно для підтримки старіших браузерів або середовищ.
- Читабельність: Використовуйте описові імена змінних та коментарі, щоб зробити ваш код більш читабельним та легким для підтримки.
- Продуктивність: Пам'ятайте про наслідки для продуктивності при об'єднанні кількох допоміжних функцій у ланцюжок. Хоча лінивість допомагає, надмірне ланцюгування все ж може створювати додаткові накладні витрати.
Висновок
Допоміжні функції асинхронних генераторів JavaScript надають потужний та елегантний спосіб створювати, трансформувати та керувати асинхронними потоками даних. Використовуючи ці допоміжні функції, розробники можуть писати більш стислий, читабельний та легкий для підтримки код для обробки складних асинхронних операцій. Розуміння основ асинхронних генераторів та ітераторів, а також функціональності кожної допоміжної функції, є важливим для ефективного використання цих інструментів у реальних додатках. Незалежно від того, чи створюєте ви конвеєри даних, обробляєте дані в реальному часі чи працюєте з асинхронними відповідями API, допоміжні функції асинхронних генераторів можуть значно спростити ваш код та покращити його загальну ефективність.